package linc.fun.openai.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.StopWatch;
import cn.hutool.core.util.RandomUtil;
import com.google.common.collect.Lists;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import linc.fun.openai.constants.ApplicationConstants;
import linc.fun.openai.constants.RabbitKeyConstants;
import linc.fun.openai.constants.RedisKeyConstants;
import linc.fun.openai.domain.dto.query.MyOrdersPageQuery;
import linc.fun.openai.domain.dto.request.ChatProductTradeRequest;
import linc.fun.openai.domain.dto.request.ProductTradeItem;
import linc.fun.openai.domain.entity.chat.*;
import linc.fun.openai.domain.vo.MyOrderVO;
import linc.fun.openai.enums.*;
import linc.fun.openai.exception.BizException;
import linc.fun.openai.manager.ChatProductStockManager;
import linc.fun.openai.mapper.*;
import linc.fun.openai.service.ChatOrderService;
import linc.fun.openai.stream.producer.StreamProducer;
import linc.fun.openai.util.ChatUserUtil;
import linc.fun.openai.util.IdGenerator;
import linc.fun.openai.util.ThrowExceptionUtil;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;

import static linc.fun.openai.domain.entity.chat.table.Tables.*;

/**
 * @author yqlin
 * @date 2023/5/8 00:05
 * @description
 */
@Slf4j
@Service
public class ChatOrderServiceImpl extends ServiceImpl<ChatOrderMapper, ChatOrderDO> implements ChatOrderService {
    @Resource
    private IdGenerator idGenerator;
    @Resource
    private ChatProductMapper chatProductMapper;
    @Resource
    private ChatOrderMapper chatOrderMapper;
    @Resource
    private ChatOrderHistoryMapper chatOrderHistoryMapper;
    @Resource
    private ChatExchangeMapper chatExchangeMapper;
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Resource
    private Redisson redisson;
    @Resource
    private ChatOrderItemMapper chatOrderItemMapper;
    @Resource
    private StreamProducer streamProducer;
    @Resource
    private TransactionTemplate transactionTemplate;
    @Resource
    private ChatProductStockManager chatProductStockManager;


    /**
     * 预先将商品库存放入Redis，数据结构是hash的形式，key:商品id value:商品库存
     * tryLock
     * if (Objects.nonNull(rLock) &&
     * rLock.isLocked() &&
     * rLock.isHeldByCurrentThread()) {    rLock.unlock(); }
     */
    @PostConstruct
    public void preloadProductStock() {
        // 移除之前的key
        Long size = redisTemplate.opsForHash().size(RedisKeyConstants.CHAT_PRODUCT_ID_STOCK_KEY);
        if (size > 0) {
            log.info("预加载，目前存在商品库存缓存数量: {}", size);
            return;
        }
        RLock rLock = redisson.getLock(RedisKeyConstants.SYNC_CHAT_PRODUCT_ID_STOCK_LOCK);
        rLock.lock(2, TimeUnit.MINUTES);
        try {
            log.info("预加载商品库存明细...");
            QueryWrapper cWrapper = QueryWrapper.create()
                    .select(ChatProduct.Id, ChatProduct.Stock, ChatProduct.Name)
                    .where(ChatProduct.Status.eq(EnableDisableStatusEnum.ENABLE));
            List<ChatProductDO> products = chatProductMapper.selectListByQuery(cWrapper);
            if (CollUtil.isEmpty(products)) {
                log.info("商品库存为空");
                return;
            }
            Map<String, Integer> productStockMap = products.stream()
                    .collect(Collectors.toMap(item -> item.getId().toString(),
                            ChatProductDO::getStock));
            redisTemplate.opsForHash().putAll(RedisKeyConstants.CHAT_PRODUCT_ID_STOCK_KEY, productStockMap);
            log.info("预加载商品库存明细ok");
        } catch (Exception e) {
            log.error("预加载商品异常", e);
        } finally {
            rLock.unlock();
        }
    }


    // 查询出未使用的兑换码进行 启用&&未被占用(代表还未属于用户)&&未被使用
//        List<ChatExchangeDO> exchanges = this.queryUnusedExchanges(req);

    // 用商品id进行分组，进行随机抽取分配兑换码，此时都处于占用状态
/*        List<ChatExchangeDO> distributeRandomUserExchanges = this.distributeRandomExchange(req, exchanges);
        log.info("分配用户id: {},兑换码信息: {}", ChatUserUtil.getUserId(), JSON.toJSONString(distributeRandomUserExchanges));

        // 从当前兑换码中选出
        // 这些兑换码在未支付订单之前都需要处于占用状态
        this.batchUpdateDistributedExchanges(distributeRandomUserExchanges);

        List<Long> distributeExchangeIds = distributeRandomUserExchanges.stream().map(ChatExchangeDO::getId).collect(Collectors.toList());*/

    /**
     * 1.预先将商品库存放入Redis
     * 2.校验合法性
     * 3.计算总金额
     * 4.组装订单
     * 5.组装历史订单
     * 6.预扣减库存
     * 7.发超时订单
     */
    @Override
    public Long createOrder(ChatProductTradeRequest req) {
        StopWatch stopWatch = new StopWatch();

        // 校验当前用户是否存在未支付的订单，如果存在就提示先支付或者取消订单
        stopWatch.start("校验当前用户是否存在未支付的订单");
        this.checkUnPaidOrder();
        stopWatch.stop();

        stopWatch.start("校验商品合法性");
        // 校验合法性，校验库存就从Redis缓存中进行，扣减库存也是从这里面进行预扣减
        List<ChatProductDO> products = this.checkProductValid(req);
        stopWatch.stop();

        // 计算总金额
        stopWatch.start("计算商品总金额");
        BigDecimal totalAmount = this.calcuTotalAmount(req, products);
        log.info("当前商品选择后的总金额: {}", totalAmount);
        stopWatch.stop();

        // 组装订单
        stopWatch.start("组装订单");
        ChatOrderDO order = this.composeOrder(totalAmount);
        stopWatch.stop();

        // 组装历史订单
        stopWatch.start("校验商品合法性");
        ChatOrderHistoryDO chatOrderHistoryDO = this.composeOrderHistory(order);
        stopWatch.stop();

        // 组装订单项
        stopWatch.start("组装订单项");
        Map<Long, ChatProductDO> productMap = products.stream().collect(Collectors.toMap(ChatProductDO::getId, Function.identity(), (v1, v2) -> v1));
        List<ChatOrderItemDO> orderItems = this.composeOrderItems(order, req.getSelectedProductItems(), productMap);
        stopWatch.stop();

        // 下单数据落库
        stopWatch.start("下单数据落库");
        Boolean ok = transactionTemplate.execute(
                status -> {
                    try {
                        chatOrderMapper.insert(order);
                        chatOrderHistoryMapper.insert(chatOrderHistoryDO);
                        chatOrderItemMapper.insertBatch(orderItems);
                    } catch (Exception e) {
                        log.error("下单数据落库异常", e);
                        status.setRollbackOnly();
                        return false;
                    }
                    return true;
                }
        );
        stopWatch.stop();

        // 事务成功
        if (Boolean.TRUE.equals(ok)) {
            // Redis库存扣减
            stopWatch.start("Redis库存扣减");
            this.doDeductionProductStock(req);
            stopWatch.stop();

            // 发送延迟队列，超时时间进行处理
            stopWatch.start("发送延迟队列，超时时间进行处理");
            this.sendOrderDelayMessage(order);
            stopWatch.stop();
        } else {
            throw BizException.of("下单异常，请联系管理员");
        }

        log.info("{}", stopWatch.prettyPrint(TimeUnit.MILLISECONDS));
        return order.getId();
    }

    private void checkUnPaidOrder() {
        long count = this.count(QueryWrapper.create()
                .where(ChatOrder.Status.in(ChatOrderStatusEnum.PENDING_PAYMENT, ChatOrderStatusEnum.PENDING_CONFIRMATION))
                .and(ChatOrder.UserId.eq(ChatUserUtil.getUserId()))
        );
        if (count > 0) {
            throw BizException.of(String.format("您还存在%d笔未支付的订单，请前往我的订单处理", count));
        }
    }

    @Override
    public Boolean checkOrderStatusFinished(Long orderId) {
        QueryWrapper queryWrapper = QueryWrapper.create()
                .where(ChatOrder.Id.eq(orderId))
                .and(ChatOrder.Status.eq(ChatOrderStatusEnum.COMPLETED));
        return this.count(queryWrapper) > 0;
    }

    @Override
    public Page<MyOrderVO> pageMyOrders(MyOrdersPageQuery query) {
        Long userId = ChatUserUtil.getUserId();
        query.setUserId(userId);
        Page<MyOrderVO> page = new Page<>(query.getPageNum(), query.getPageSize());
        long total = chatOrderMapper.queryMyOrderTotal(query);
        if (total == 0) return page;
        List<MyOrderVO> records = chatOrderMapper.queryMyOrdersByUserId(query);
        page.setRecords(records);
        page.setTotalRow(total);
        return page;
    }

    @Override
    public void closeMyOrder(Long orderId) {
        ChatOrderDO order = this.getById(orderId);
        if (Objects.isNull(order)) {
            throw BizException.of("当前订单不存在");
        }

        // 待确认，代付款
        if (ChatOrderStatusEnum.PENDING_CONFIRMATION.equals(order.getStatus())
                || ChatOrderStatusEnum.PENDING_PAYMENT.equals(order.getStatus())
        ) {
            order.setStatus(ChatOrderStatusEnum.CLOSED);
            order.setRemark(ApplicationConstants.CHAT_ORDER_ACTIVE_CLOSE_ORDER_REMARK);
            order.setUpdateTime(LocalDateTime.now());
            this.updateById(order);

            List<ChatOrderItemDO> orderItems = chatOrderItemMapper.selectListByQuery(QueryWrapper.create()
                    .where(ChatOrderItem.OrderId.eq(orderId))
            );

            // 归还预减库存
            chatProductStockManager.returnProductStock(orderItems);
        }

    }


    private void sendOrderDelayMessage(ChatOrderDO order) {
        final int expireMinutes = 30;
        // 30min过期
        if (!streamProducer.sendDelayMessage(order.getId().toString(), RabbitKeyConstants.CHAT_ORDER_DLQ_BINDING_NAME, expireMinutes * 60)) {
            log.error("发送延迟消息失败");
        }
    }


    private void doDeductionProductStock(ChatProductTradeRequest req) {
        Map<Object, Object> cachedProductStockMap = redisTemplate.opsForHash().entries(RedisKeyConstants.CHAT_PRODUCT_ID_STOCK_KEY);
        Map<Long, Integer> selectedProductMap = req.getSelectedProductMap();
        for (Map.Entry<Object, Object> entry : cachedProductStockMap.entrySet()) {
            Long productId = Long.valueOf((String) entry.getKey());
            Integer deductionStock = selectedProductMap.get(productId);
            if (Objects.isNull(deductionStock)) continue;
            // 对商品ID进行加锁
            RLock rLock = redisson.getLock(RedisKeyConstants.DEDUCTION_CHAT_PRODUCT_STOCK_LOCK_KEY + productId);
            try {
                boolean tryLock = rLock.tryLock(1, 5 * 60, TimeUnit.SECONDS);
                if (tryLock) {
                    Integer deductionAfterStock = (Integer) entry.getValue() - deductionStock;
                    entry.setValue(deductionAfterStock);
                }
            } catch (Exception e) {
                log.error("扣减库存异常", e);
                throw BizException.CHAT_PRODUCT_DEDUCTION_STOCK_ERROR;
            } finally {
                if (Objects.nonNull(rLock) &&
                        rLock.isLocked() &&
                        rLock.isHeldByCurrentThread()) {
                    rLock.unlock();
                }
            }
        }
        redisTemplate.opsForHash().putAll(RedisKeyConstants.CHAT_PRODUCT_ID_STOCK_KEY, cachedProductStockMap);
    }

    @NotNull
    private List<ChatOrderItemDO> composeOrderItems(ChatOrderDO order, List<ProductTradeItem> selectedProductItems, Map<Long, ChatProductDO> productMap) {
        return selectedProductItems.stream().map(selectedProduct -> {
            ChatProductDO currentProduct = productMap.get(selectedProduct.getProductId());
            ChatOrderItemDO orderItem = new ChatOrderItemDO();
            orderItem.setId(idGenerator.getId());
            orderItem.setOrderId(order.getId());
            orderItem.setProductId(selectedProduct.getProductId());
            orderItem.setExchangeNum(selectedProduct.getNum());
            orderItem.setProductName(currentProduct.getName());
            orderItem.setProductPrice(currentProduct.getPrice());
            orderItem.setCreateTime(LocalDateTime.now());
            orderItem.setUpdateTime(LocalDateTime.now());
            orderItem.setIsDeleted(LogicDeleteEnum.NORMAL);
            return orderItem;
        }).toList();
    }


    @NotNull
    private List<ChatExchangeDO> distributeRandomExchange(ChatProductTradeRequest req, List<ChatExchangeDO> exchanges) {
        Map<Long, List<ChatExchangeDO>> exchangeMap = exchanges.stream().collect(Collectors.groupingBy(ChatExchangeDO::getProductId));
        List<ChatExchangeDO> attributeUserExchanges = Lists.newArrayList();
        for (Map.Entry<Long, List<ChatExchangeDO>> entry : exchangeMap.entrySet()) {
            Integer randomNum = req.getSelectedProductMap().get(entry.getKey());
            attributeUserExchanges.addAll(RandomUtil.randomEleList(entry.getValue(), randomNum));
        }
        return attributeUserExchanges;
    }

    @NotNull
    private List<ChatExchangeDO> queryUnusedExchanges(ChatProductTradeRequest req) {
        QueryWrapper eWrapper = QueryWrapper.create()
                .where(ChatExchange.ProductId.in(req.getSelectedProductIds()))
                .and(ChatExchange.Status.eq(EnableDisableStatusEnum.ENABLE))
                .and(ChatExchange.IsUsed.eq(false));
        List<ChatExchangeDO> exchanges = chatExchangeMapper.selectListByQuery(eWrapper);
        if (CollUtil.isEmpty(exchanges)) {
            throw BizException.CHAT_EXCHANGES_OUT_OF_STOCK;
        }
        return exchanges;
    }


    @NotNull
    private ChatOrderHistoryDO composeOrderHistory(ChatOrderDO chatOrderDO) {
        ChatOrderHistoryDO chatOrderHistoryDO = new ChatOrderHistoryDO();
        chatOrderHistoryDO.setId(idGenerator.getId());
        chatOrderHistoryDO.setUserId(ChatUserUtil.getUserId());
        chatOrderHistoryDO.setOrderId(chatOrderDO.getId());
        chatOrderHistoryDO.setOrderStatus(chatOrderDO.getStatus());
        chatOrderHistoryDO.setPayStatus(ChatOrderPayStatusEnum.PAYMENT_PENDING);
        chatOrderHistoryDO.setTotalAmount(chatOrderDO.getTotalAmount());
        chatOrderHistoryDO.setPayAmount(chatOrderDO.getPayAmount());
        chatOrderHistoryDO.setCreateTime(LocalDateTime.now());
        chatOrderHistoryDO.setUpdateTime(LocalDateTime.now());
        return chatOrderHistoryDO;
    }

    @NotNull
    private ChatOrderDO composeOrder(BigDecimal totalAmount) {
        ChatOrderDO chatOrderDO = new ChatOrderDO();
        chatOrderDO.setId(idGenerator.getId());
        chatOrderDO.setUserId(ChatUserUtil.getUserId());
        chatOrderDO.setTotalAmount(totalAmount);
        chatOrderDO.setPayAmount(totalAmount);
        chatOrderDO.setStatus(ChatOrderStatusEnum.PENDING_PAYMENT);
        chatOrderDO.setType(ChatOrderTypeEnum.NORMAL);
        chatOrderDO.setCreateTime(LocalDateTime.now());
        chatOrderDO.setUpdateTime(LocalDateTime.now());
        return chatOrderDO;
    }

    private BigDecimal calcuTotalAmount(ChatProductTradeRequest req, List<ChatProductDO> products) {
        BigDecimal totalAmount = BigDecimal.ZERO;
        for (ChatProductDO product : products) {
            Map<Long, Integer> productMap = req.getSelectedProductMap();
            Integer curProductNum = productMap.get(product.getId());
            BigDecimal curProductAmount = new BigDecimal(curProductNum).multiply(product.getPrice());
            totalAmount = totalAmount.add(curProductAmount);
        }
        return totalAmount;
    }


    private List<ChatProductDO> queryChatProductsByIds(List<Long> productIds) {
        QueryWrapper cWrapper = QueryWrapper.create()
                .where(ChatProduct.Id.in(productIds))
                .and(ChatProduct.Status.eq(EnableDisableStatusEnum.ENABLE));
        return chatProductMapper.selectListByQuery(cWrapper);
    }

    private List<ChatProductDO> checkProductValid(ChatProductTradeRequest req) {
        List<ChatProductDO> products = this.queryChatProductsByIds(req.getSelectedProductIds());
        ThrowExceptionUtil.isFalse(products.size() == req.getSelectedProductIds().size()).throwMessage("存在不合法的商品信息，请勿非法篡改");
        Map<Long, ChatProductDO> productMap = products.stream().collect(Collectors.toMap(ChatProductDO::getId, Function.identity()));
        this.checkProductStock(productMap, req);
        return products;
    }

    private void checkProductStock(Map<Long, ChatProductDO> productMap, ChatProductTradeRequest req) {
        Map<Object, Object> cachedProductStockMap = redisTemplate.opsForHash().entries(RedisKeyConstants.CHAT_PRODUCT_ID_STOCK_KEY);
        if (CollUtil.isEmpty(cachedProductStockMap)) {
            log.info("商品库存明细数据丢失");
            throw BizException.DATA_HAS_BEEN_LOOSED;
        }
        for (ProductTradeItem selectedProductItem : req.getSelectedProductItems()) {
            String productName = productMap.get(selectedProductItem.getProductId()).getName();
            Integer stock = (Integer) cachedProductStockMap.get(String.valueOf(selectedProductItem.getProductId()));
            if (Objects.isNull(stock)) stock = 0;
            ThrowExceptionUtil.isFalse(selectedProductItem.getNum() <= stock).throwMessage(String.format("[%s]库存不足", productName));
        }
    }
}
